@namespace Oqtane.Components
@using System.Net
@using System.Security.Claims
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Http.Extensions
@using Microsoft.AspNetCore.Antiforgery
@using Microsoft.AspNetCore.Localization
@using Microsoft.Net.Http.Headers
@using Microsoft.Extensions.Primitives
@using Microsoft.AspNetCore.Authentication
@using Oqtane.Client
@using Oqtane.UI
@using Oqtane.Repository
@using Oqtane.Infrastructure
@using Oqtane.Security
@using Oqtane.Models
@using Oqtane.Shared
@using Oqtane.Themes
@using Oqtane.Extensions
@using System.Globalization
@inject NavigationManager NavigationManager
@inject IAntiforgery Antiforgery
@inject IConfigManager ConfigManager
@inject ITenantManager TenantManager
@inject ISiteService SiteService
@inject IPageRepository PageRepository
@inject IThemeRepository ThemeRepository
@inject ILanguageRepository LanguageRepository
@inject ILocalizationManager LocalizationManager
@inject IAliasRepository AliasRepository
@inject IUrlMappingRepository UrlMappingRepository
@inject IVisitorRepository VisitorRepository
@inject IJwtManager JwtManager
@inject ICookieConsentService CookieConsentService
@inject ISettingService SettingService

@if (_initialized)
{
    <!DOCTYPE html>
    <html lang="@_language">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <base href="/" />
        <link rel="stylesheet" href="css/app.css?v=@_fingerprint" />
        @if (_scripts.Contains("PWA Manifest"))
        {
            <link id="app-manifest" rel="manifest" />
        }
        @((MarkupString)_styleSheets)
        <link id="app-stylesheet-page" />
        <link id="app-stylesheet-module" />
        @if (_renderMode == RenderModes.Static)
        {
            <Head RenderMode="@_renderMode" Runtime="@_runtime" />
        }
        else
        {
            <Head RenderMode="@_renderMode" Runtime="@_runtime" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(_runtime, _prerender)" />
        }
        @((MarkupString)_headResources)
    </head>
    <body @attributes="_bodyAttributes">
        @if (string.IsNullOrEmpty(_message))
        {
            @if (_renderMode == RenderModes.Static)
            {
                <Routes PageState="@_pageState" RenderMode="@_renderMode" Runtime="@_runtime" AntiForgeryToken="@_antiForgeryToken" AuthorizationToken="@_authorizationToken" Platform="@_platform" />
            }
            else
            {
                <Routes PageState="@_pageState" RenderMode="@_renderMode" Runtime="@_runtime" AntiForgeryToken="@_antiForgeryToken" AuthorizationToken="@_authorizationToken" Platform="@_platform" @rendermode="InteractiveRenderMode.GetInteractiveRenderMode(_runtime, _prerender)" />
            }

            <script src="_framework/blazor.web.js"></script>
            <script src="js/app.js?v=@_fingerprint"></script>
            <script src="js/loadjs.min.js?v=@_fingerprint"></script>
            <script src="js/interop.js?v=@_fingerprint"></script>

            @((MarkupString)_scripts)
            @((MarkupString)_bodyResources)
            @if (_renderMode == RenderModes.Static)
            {
                <page-script src="./js/reload.js?v=@_fingerprint"></page-script>
            }
        }
        else
        {
            <div class="app-alert">@_message</div>
        }
    </body>
    </html>
}

@code {
    private bool _initialized = false;
    private string _renderMode = RenderModes.Interactive;
    private string _runtime = Runtimes.Server;
    private bool _prerender = true;
    Dictionary<string, object> _bodyAttributes { get; set; } = new();
    private string _fingerprint = "";
    private int _visitorId = -1;
    private string _antiForgeryToken = "";
    private string _remoteIPAddress = "";
    private string _platform = "";
    private string _authorizationToken = "";
    private string _language = "en";
    private string _headResources = "";
    private string _bodyResources = "";
    private string _styleSheets = "";
    private string _scripts = "";
    private string _message = "";
    private bool _allowCookies;
    private PageState _pageState;

    //  CascadingParameter is required to access HttpContext
    [CascadingParameter]
    HttpContext Context { get; set; }

    protected override async Task OnInitializedAsync()
    {
        _antiForgeryToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
        _remoteIPAddress = Context.Connection.RemoteIpAddress?.ToString() ?? "";
        _platform = System.Runtime.InteropServices.RuntimeInformation.OSDescription;

        // if framework is installed
        if (ConfigManager.IsInstalled())
        {
            var alias = TenantManager.GetAlias();
            if (alias != null)
            {
                var url = WebUtility.UrlDecode(Context.Request.GetEncodedUrl());

                if (!alias.IsDefault)
                {
                    HandleDefaultAliasRedirect(alias, url);
                }

                var site = await SiteService.GetSiteAsync(alias.SiteId);
                if (site != null && (!site.IsDeleted || url.Contains("admin/site")) && site.RenderMode != RenderModes.Headless)
                {
                    _renderMode = site.RenderMode;
                    _runtime = site.Runtime;
                    _prerender = site.Prerender;
                    if (_renderMode == RenderModes.Static && !site.EnhancedNavigation)
                    {
                        _bodyAttributes.Add("data-enhance-nav", "false");
                    }
                    _fingerprint = site.Fingerprint;

                    var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
                    _allowCookies = string.IsNullOrEmpty(cookieConsentSettings) || await CookieConsentService.CanTrackAsync(cookieConsentSettings == "optout");

                    var modules = new List<Module>();

                    Route route = new Route(url, alias.Path);
                    var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
                    if (page == null && route.PagePath == "") // naked path refers to site home page
                    {
                        if (site.HomePageId != null)
                        {
                            page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId);
                        }
                        if (page == null)
                        {
                            // fallback to use the first page in the collection
                            page = site.Pages.FirstOrDefault();
                        }
                    }
                    if (page == null)
                    {
                        // personalized pages need to be retrieved using path
                        page = PageRepository.GetPage(route.PagePath, site.SiteId);
                    }
                    if (page == null || page.IsDeleted)
                    {
                        HandlePageNotFound(site, page, route);
                        return;
                    }
                    else
                    {
                        modules = await SiteService.GetModulesAsync(site.SiteId, page.PageId);
                    }

                    if (site.VisitorTracking && _allowCookies)
                    {
                        TrackVisitor(site.SiteId);
                    }

                    // get jwt token for downstream APIs
                    if (Context.User.Identity.IsAuthenticated)
                    {
                        await GetJwtToken(alias);
                    }

                    // includes resources
                    var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action);
                    ManageStyleSheets(resources);
                    ManageScripts(resources, alias);

                    // generate scripts
                    if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
                    {
                        _scripts += CreatePWAScript(alias, site, route);
                    }

                    // set culture if not specified
                    string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
                    if (cultureCookie == null)
                    {
                        // get default language for site
                        if (site.Languages.Any())
                        {
                            // use default language if specified otherwise use first language in collection
                            cultureCookie = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
                        }
                        else
                        {
                            // fallback language
                            cultureCookie = LocalizationManager.GetDefaultCulture();
                        }
                        // convert language code to culture cookie format (ie. "c=en|uic=en")
                        cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(cultureCookie));
                        SetLocalizationCookie(cultureCookie);
                    }

                    // set language for page
                    if (!string.IsNullOrEmpty(cultureCookie))
                    {
                        _language = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie).Culture.Name;
                    }

                    // create initial PageState
                    _pageState = new PageState
                    {
                        Alias = alias,
                        Site = site,
                        Page = page,
                        Modules = modules,
                        User = site.User,
                        Uri = new Uri(url, UriKind.Absolute),
                        Route = route,
                        QueryString = Utilities.ParseQueryString(route.Query),
                        UrlParameters = route.UrlParameters,
                        ModuleId = -1,
                        Action = "",
                        EditMode = false,
                        LastSyncDate = DateTime.MinValue,
                        RenderMode = _renderMode,
                        Runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), _runtime),
                        VisitorId = _visitorId,
                        RemoteIPAddress = _remoteIPAddress,
                        ReturnUrl = "",
                        IsInternalNavigation = false,
                        RenderId = Guid.NewGuid(),
                        Refresh = true,
                        AllowCookies = _allowCookies
                    };
                }
                else
                {
                    _message = "Site Is Disabled";
                }
            }
            else
            {
                _message = "Site Not Configured Correctly - No Matching Alias Exists For Host Name";
            }
        }
        _initialized = true;
    }

    private void HandleDefaultAliasRedirect(Alias alias, string url)
    {
        // get aliases for site and tenant
        var aliases = AliasRepository.GetAliases().Where(item => item.TenantId == alias.TenantId && item.SiteId == alias.SiteId);
        // get first default alias
        var defaultAlias = aliases.Where(item => item.IsDefault).FirstOrDefault();
        if (defaultAlias != null)
        {
            // redirect to default alias
            NavigationManager.NavigateTo(url.Replace(alias.Name, defaultAlias.Name), true);
        }
        else // no default alias specified - use first alias
        {
            defaultAlias = aliases.FirstOrDefault();
            if (defaultAlias != null && alias.Name.Trim() != defaultAlias.Name.Trim())
            {
                // redirect to first alias
                NavigationManager.NavigateTo(url.Replace(alias.Name, defaultAlias.Name), true);
            }
        }
    }

    private void HandlePageNotFound(Site site, Page page, Route route)
    {
        // page not found - look for url mapping
        var urlMapping = UrlMappingRepository.GetUrlMapping(site.SiteId, route.PagePath);
        if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
        {
            // redirect to mapped url
            var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + (!urlMapping.MappedUrl.StartsWith("/") ? "/" : "") + urlMapping.MappedUrl + ((!urlMapping.MappedUrl.Contains("?")) ? route.Query : "");
            NavigationManager.NavigateTo(url, true);
        }
        else // no url mapping exists
        {
            if (route.PagePath != "404")
            {
                // handle not found request in static mode
                if(_renderMode == RenderModes.Static)
                {
                    NavigationManager.NotFound();
                }
                else
                {
                    // redirect to 404 page
                    NavigationManager.NavigateTo(route.SiteUrl + "/404", true);
                }
            }
        }
    }

    private void TrackVisitor(int SiteId)
    {
        try
        {
            // get request attributes
            string useragent = (Context.Request.Headers[HeaderNames.UserAgent] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.UserAgent] : "(none)";
            useragent = (useragent.Length > 256) ? useragent.Substring(0, 256) : useragent;
            string language = (Context.Request.Headers[HeaderNames.AcceptLanguage] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.AcceptLanguage] : "";
            language = (language.Contains(",")) ? language.Substring(0, language.IndexOf(",")) : language;
            language = (language.Contains(";")) ? language.Substring(0, language.IndexOf(";")) : language;
            language = (language.Trim().Length == 0) ? "??" : language;

            // filter
            var settings = Context.GetSiteSettings();
            var filter = settings.GetValue("VisitorFilter", Constants.DefaultVisitorFilter);
            foreach (string term in filter.ToLower().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sValue => sValue.Trim()).ToArray())
            {
                if (_remoteIPAddress.ToLower().Contains(term) || useragent.ToLower().Contains(term) || language.ToLower().Contains(term))
                {
                    return;
                }
            }

            // get other request attributes
            string url = Context.Request.GetEncodedUrl();
            string referrer = (Context.Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.Referer] : "";
            int? userid = Context.User.UserId();
            userid = (userid == -1) ? null : userid;

            // get cookie value
            var visitorCookieName = Constants.VisitorCookiePrefix + SiteId.ToString();
            var visitorCookieValue = Context.Request.Cookies[visitorCookieName];
            DateTime expiry = DateTime.MinValue;
            if (visitorCookieValue != null && visitorCookieValue.Contains("|"))
            {
                // visitor cookies contain the visitor id and an expiry date separated by a pipe symbol
                var values = visitorCookieValue.Split('|');
                int.TryParse(values[0], out _visitorId);
                DateTime.TryParseExact(values[1], "M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture, DateTimeStyles.None, out expiry);
            }
            else // legacy cookie format
            {
                int.TryParse(visitorCookieValue, out _visitorId);
            }
            bool setcookie = false;
            Visitor visitor = null;

            if (_visitorId <= 0)
            {
                // if enabled use IP Address correlation
                var correlate = bool.Parse(settings.GetValue("VisitorCorrelation", "true"));
                if (correlate)
                {
                    visitor = VisitorRepository.GetVisitor(SiteId, _remoteIPAddress);
                    if (visitor != null)
                    {
                        _visitorId = visitor.VisitorId;
                        setcookie = true;
                    }
                }
            }

            if (_visitorId <= 0)
            {
                // create new visitor
                visitor = new Visitor();
                visitor.SiteId = SiteId;
                visitor.IPAddress = _remoteIPAddress;
                visitor.UserAgent = useragent;
                visitor.Language = language;
                visitor.Url = url;
                visitor.Referrer = referrer;
                visitor.UserId = userid;
                visitor.Visits = 1;
                visitor.CreatedOn = DateTime.UtcNow;
                visitor.VisitedOn = DateTime.UtcNow;
                visitor = VisitorRepository.AddVisitor(visitor);
                _visitorId = visitor.VisitorId;
                setcookie = true;
            }
            else
            {
                // check expiry
                if (DateTime.UtcNow > expiry)
                {
                    if (visitor == null)
                    {
                        // get visitor if not previously loaded
                        visitor = VisitorRepository.GetVisitor(_visitorId);
                    }
                    if (visitor != null)
                    {
                        // update visitor
                        visitor.IPAddress = _remoteIPAddress;
                        visitor.UserAgent = useragent;
                        visitor.Language = language;
                        visitor.Url = url;
                        if (!string.IsNullOrEmpty(referrer))
                        {
                            visitor.Referrer = referrer;
                        }
                        if (userid != null)
                        {
                            visitor.UserId = userid;
                        }
                        visitor.Visits += 1;
                        visitor.VisitedOn = DateTime.UtcNow;
                        VisitorRepository.UpdateVisitor(visitor);
                        setcookie = true;
                    }
                    else
                    {
                        // remove cookie if visitor does not exist
                        Context.Response.Cookies.Delete(visitorCookieName);
                    }
                }
            }

            // set cookie
            if (setcookie)
            {
                expiry = DateTime.UtcNow.AddMinutes(int.Parse(settings.GetValue("VisitorDuration", "5")));

                Context.Response.Cookies.Append(
                    visitorCookieName,
                    $"{_visitorId}|{expiry.ToString("M/d/yyyy hh:mm:ss tt", CultureInfo.InvariantCulture)}",
                    new CookieOptions()
                    {
                        Expires = DateTimeOffset.UtcNow.AddYears(10),
                        IsEssential = true,
                        SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
                        Secure = true, // Ensure the cookie is only sent over HTTPS
                        HttpOnly = true // Optional: Helps mitigate XSS attacks
                    }
                );
            }
        }
        catch
        {
            // error tracking visitor
        }
    }

    private async Task GetJwtToken(Alias alias)
    {
        // bearer token may have been provided by remote Identity Provider and persisted using SaveTokens = true
        _authorizationToken = await Context.GetTokenAsync("access_token");
        if (string.IsNullOrEmpty(_authorizationToken))
        {
            // generate bearer token if a secret has been configured in User Settings
            var sitesettings = Context.GetSiteSettings();
            var secret = sitesettings.GetValue("JwtOptions:Secret", "");
            if (!string.IsNullOrEmpty(secret))
            {
                _authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
            }
        }
    }

    private string CreatePWAScript(Alias alias, Site site, Route route)
    {
        return Environment.NewLine +
        "<script>" + Environment.NewLine +
        "    // PWA Manifest" + Environment.NewLine +
        "    setTimeout(() => {" + Environment.NewLine +
        "        var manifest = {" + Environment.NewLine +
        "            \"name\": \"" + site.Name + "\"," + Environment.NewLine +
        "            \"short_name\": \"" + site.Name + "\"," + Environment.NewLine +
        "            \"start_url\": \"" + route.SiteUrl + "/\"," + Environment.NewLine +
        "            \"display\": \"standalone\"," + Environment.NewLine +
        "            \"background_color\": \"#fff\"," + Environment.NewLine +
        "            \"description\": \"" + site.Name + "\"," + Environment.NewLine +
        "            \"icons\": [{" + Environment.NewLine +
        "                \"src\": \"" + route.RootUrl + Utilities.FileUrl(alias, site.PwaAppIconFileId.Value) + "\"," + Environment.NewLine +
        "                \"sizes\": \"192x192\"," + Environment.NewLine +
        "                \"type\": \"image/png\"" + Environment.NewLine +
        "                }, {" + Environment.NewLine +
        "                \"src\": \"" + route.RootUrl + Utilities.FileUrl(alias, site.PwaSplashIconFileId.Value) + "\"," + Environment.NewLine +
        "                \"sizes\": \"512x512\"," + Environment.NewLine +
        "                \"type\": \"image/png\"" + Environment.NewLine +
        "            }]" + Environment.NewLine +
        "       };" + Environment.NewLine +
        "       const serialized = JSON.stringify(manifest);" + Environment.NewLine +
        "       const blob = new Blob([serialized], {type: 'application/javascript'});" + Environment.NewLine +
        "       const url = URL.createObjectURL(blob);" + Environment.NewLine +
        "       document.getElementById('app-manifest').setAttribute('href', url);" + Environment.NewLine +
        "    }, 1000);" + Environment.NewLine +
        "</script>" + Environment.NewLine +
        "<script>" + Environment.NewLine +
        "    // PWA Service Worker" + Environment.NewLine +
        "    if ('serviceWorker' in navigator) {" + Environment.NewLine +
        "        navigator.serviceWorker.register('/service-worker.js').then(function(registration) {" + Environment.NewLine +
        "            console.log('ServiceWorker Registration Successful');" + Environment.NewLine +
        "        }).catch (function(err) {" + Environment.NewLine +
        "            console.log('ServiceWorker Registration Failed ', err);" + Environment.NewLine +
        "        });" + Environment.NewLine +
        "    };" + Environment.NewLine +
        "</script>" + Environment.NewLine;
    }

    private void AddScript(Resource resource, Alias alias)
    {
        var script = CreateScript(resource, alias);
        if (resource.Location == Shared.ResourceLocation.Head && resource.LoadBehavior != ResourceLoadBehavior.BlazorPageScript)
        {
            if (!_headResources.Contains(script))
            {
                _headResources += script + Environment.NewLine;
            }
        }
        else
        {
            if (!_bodyResources.Contains(script))
            {
                _bodyResources += script + Environment.NewLine;
            }
        }
    }

    private string CreateScript(Resource resource, Alias alias)
    {
        if (resource.LoadBehavior == ResourceLoadBehavior.BlazorPageScript)
        {
            return "<page-script src=\"" + resource.Url + "\"></page-script>";
        }
        else
        {
            var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;

            var dataAttributes = "";
            if (!resource.DataAttributes.ContainsKey("data-reload"))
            {
                switch (resource.LoadBehavior)
                {
                    case ResourceLoadBehavior.Once: 
                        dataAttributes += " data-reload=\"once\"";
                        break;
                    case ResourceLoadBehavior.Always:
                        dataAttributes += " data-reload=\"always\"";
                        break;
                }
            }
            if (resource.DataAttributes != null && resource.DataAttributes.Count > 0)
            {
                foreach (var attribute in resource.DataAttributes)
                {
                    dataAttributes += " " + attribute.Key + "=\"" + attribute.Value + "\"";
                }
            }

            return "<script src=\"" + url + "\"" +
                ((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
                ((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
                ((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
                ((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
                "></script>";
        }
    }

    private void SetLocalizationCookie(string cookieValue)
    {
        var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
        {
            Expires = DateTimeOffset.UtcNow.AddYears(1),
            SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
            Secure = true, // Ensure the cookie is only sent over HTTPS
            HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
        };

        Context.Response.Cookies.Append(
            Shared.CookieRequestCultureProvider.DefaultCookieName,
            cookieValue,
            cookieOptions
        );
    }

    private async Task<List<Resource>> GetPageResources(Alias alias, Site site, Page page, List<Module> modules, int moduleid, string action)
    {
        var resources = new List<Resource>();

        var themeType = (string.IsNullOrEmpty(page.ThemeType)) ? site.DefaultThemeType : page.ThemeType;
        var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType));
        if (theme != null)
        {
            resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode);
        }
        else
        {
            // fallback to default Oqtane theme
            theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme));
            resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode);
        }
        var type = Type.GetType(themeType);
        if (type != null)
        {
            var obj = Activator.CreateInstance(type) as IThemeControl;
            if (obj != null)
            {
                resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, theme.Fingerprint, site.RenderMode);
            }
        }
        // theme settings components are dynamically loaded within the framework Page Management module
        if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
        {
            var settingsType = Type.GetType(theme.ThemeSettingsType);
            if (settingsType != null)
            {
                var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
                resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint, site.RenderMode);
            }
        }

        foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid))
        {
            var typename = "";
            if (module.ModuleDefinition != null)
            {
                // handle default action
                if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
                {
                    action = module.ModuleDefinition.DefaultAction;
                }

                // get typename template
                typename = module.ModuleDefinition.ControlTypeTemplate;
                if (module.ModuleDefinition.ControlTypeRoutes != "")
                {
                    // process custom action routes
                    foreach (string route in module.ModuleDefinition.ControlTypeRoutes.Split(';', StringSplitOptions.RemoveEmptyEntries))
                    {
                        if (route.StartsWith(action + "="))
                        {
                            typename = route.Replace(action + "=", "");
                            break;
                        }
                    }
                }
            }

            // create typename
            if (Constants.DefaultModuleActions.Contains(action, StringComparer.OrdinalIgnoreCase))
            {
                typename = Constants.DefaultModuleActionsTemplate.Replace(Constants.ActionToken, action);
            }
            else
            {
                typename = typename.Replace(Constants.ActionToken, action);
            }

            // ensure component exists and implements IModuleControl
            module.ModuleType = "";
            var moduletype = Type.GetType(typename, false, true); // case insensitive
            if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
            {
                module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name
            }
            if (moduletype != null && module.ModuleType != "")
            {
                var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                if (moduleobject != null)
                {
                    if (module.ModuleDefinition != null)
                    {
                        resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition.Fingerprint, moduleobject.RenderMode);
                    }
                    resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, moduleobject.RenderMode);

                    // settings components are dynamically loaded within the framework Settings module
                    if (action.ToLower() == "settings" && module.ModuleDefinition != null)
                    {
                        // module settings component
                        var settingsType = "";
                        if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
                        {
                            // module settings type explicitly declared in IModule interface
                            settingsType = module.ModuleDefinition.SettingsType;
                        }
                        else
                        {
                            // legacy support - module settings type determined by convention
                            settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
                        }
                        moduletype = Type.GetType(settingsType, false, true);
                        if (moduletype != null)
                        {
                            moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                            resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, site.RenderMode);
                        }

                        // container settings component
                        if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
                        {
                            moduletype = Type.GetType(theme.ContainerSettingsType);
                            if (moduletype != null)
                            {
                                moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
                                resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint, site.RenderMode);
                            }
                        }
                    }
                }
            }
        }

        if (site.RenderMode == RenderModes.Interactive)
        {
            // site level resources for modules in site
            var sitemodules = await SiteService.GetModulesAsync(site.SiteId, -1);
            foreach (var module in sitemodules.GroupBy(item => item.ModuleDefinition?.ModuleDefinitionName).Select(group => group.First()).ToList())
            {
                if (module.ModuleDefinition?.Resources != null)
                {
                    resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint, site.RenderMode);
                }
            }
        }

        return resources;
    }

    private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint, string rendermode)
    {
        if (resources != null)
        {
            foreach (var resource in resources)
            {
                if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site)
                {
                    if (resource.Url.StartsWith("~"))
                    {
                        resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/");
                    }
                    if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl))
                    {
                        resource.Url = alias.BaseUrl + resource.Url;
                    }

                    // ensure resource does not exist already
                    if (!pageresources.Exists(item => Utilities.GetUrlPath(item.Url).ToLower() == Utilities.GetUrlPath(resource.Url).ToLower()))
                    {
                        pageresources.Add(resource.Clone(level, name, fingerprint));
                    }
                }
            }
        }
        return pageresources;
    }

    private void ManageStyleSheets(List<Resource> resources)
    {
        if (resources != null)
        {
            // include stylesheets to prevent FOUC
            string batch = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff");
            int count = 0;
            foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Stylesheet))
            {
                count++;
                string id = "id=\"app-stylesheet-" + ResourceLevel.Page.ToString().ToLower() + "-" + batch + "-" + count.ToString("00") + "\" ";
                _styleSheets += "<link " + id + "rel=\"stylesheet\"" + 
                    (!string.IsNullOrEmpty(resource.Integrity) ? " integrity=\"" + resource.Integrity + "\"" : "") + 
                    (!string.IsNullOrEmpty(resource.CrossOrigin) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") + 
                    " href=\"" + resource.Url + "\" type=\"text/css\"/>" + Environment.NewLine; // href at end of element due to enhanced navigation patch algorithm
            }
        }
    }

    private void ManageScripts(List<Resource> resources, Alias alias)
    {
        if (resources != null)
        {
            foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Script))
            {
                if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Static)
                {
                    AddScript(resource, alias);
                }
            }
        }
    }
}
